// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace iPanel.Otlp.Model;

/// <summary>
/// 表示一个OpenTelemetry协议（OTLP）跟踪对象。
/// 该类包含了跟踪的基本信息，如跟踪ID、完整名称、跨度集合等，以及用于计算跟踪深度、添加跨度等功能的方法。
/// </summary>
[DebuggerDisplay("{DebuggerToString(),nq}")]
public class OtlpTrace
{
    /// <summary>
    /// 根跨度对象，用于标识跟踪的根节点。
    /// </summary>
    private OtlpSpan? _rootSpan;

    /// <summary>
    /// 获取跟踪的唯一键，以只读内存字节形式表示。
    /// </summary>
    public ReadOnlyMemory<byte> Key { get; }

    /// <summary>
    /// 获取跟踪的唯一标识符，以十六进制字符串形式表示。
    /// </summary>
    public string TraceId { get; }

    /// <summary>
    /// 获取或私有设置跟踪的完整名称，通常由根跨度的信息构建而成。
    /// </summary>
    public string FullName { get; private set; }

    /// <summary>
    /// 获取跟踪中的第一个跨度对象。
    /// 假设跟踪中至少有一个跨度。
    /// </summary>
    public OtlpSpan FirstSpan => Spans[0];

    /// <summary>
    /// 获取跟踪的时间戳，该时间戳取自第一个跨度的开始时间。
    /// </summary>
    public DateTime TimeStamp => FirstSpan.StartTime;

    /// <summary>
    /// 获取跟踪的根跨度对象，如果不存在根跨度，则返回null。
    /// </summary>
    public OtlpSpan? RootSpan => _rootSpan;

    /// <summary>
    /// 获取跟踪的根跨度对象，如果根跨度不存在，则返回第一个跨度对象。
    /// </summary>
    public OtlpSpan RootOrFirstSpan => RootSpan ?? FirstSpan;

    /// <summary>
    /// 获取跟踪的持续时间，即从第一个跨度的开始时间到所有跨度中最晚结束时间的时间差。
    /// </summary>
    public TimeSpan Duration
    {
        get
        {
            // 获取第一个跨度的开始时间作为跟踪的开始时间
            var start = FirstSpan.StartTime;
            // 初始化结束时间为默认值
            DateTime end = default;
            // 遍历所有跨度，找到最晚的结束时间
            foreach (var span in Spans)
            {
                if (span.EndTime > end)
                {
                    end = span.EndTime;
                }
            }
            // 计算并返回持续时间
            return end - start;
        }
    }

    public OtlpSpanCollection Spans { get; } = new OtlpSpanCollection();

    /// <summary>
    /// 计算指定跨度的深度。
    /// 深度定义为从指定跨度到根跨度的路径上的跨度数量。
    /// </summary>
    /// <param name="span">要计算深度的跨度对象。</param>
    /// <returns>指定跨度的深度。</returns>
    public int CalculateDepth(OtlpSpan span)
    {
        // 初始化深度为0
        var depth = 0;
        // 从指定跨度开始向上遍历
        var currentSpan = span;
        // 当当前跨度不为空时，继续向上遍历
        while (currentSpan != null)
        {
            // 深度加1
            depth++;
            // 获取当前跨度的父跨度
            currentSpan = currentSpan.GetParentSpan();
        }
        return depth;
    }

    /// <summary>
    /// 计算跟踪中所有跨度的最大深度。
    /// </summary>
    /// <returns>跟踪中所有跨度的最大深度。</returns>
    public int CalculateMaxDepth() => Spans.Max(CalculateDepth);

    /// <summary>
    /// 向跟踪中添加一个跨度。
    /// 确保跨度ID的唯一性，并按开始时间排序插入。
    /// 检查是否存在循环引用，如果存在则抛出异常。
    /// 确定根跨度并构建跟踪的完整名称。
    /// </summary>
    /// <param name="span">要添加的跨度对象。</param>
    /// <exception cref="InvalidOperationException">
    /// 如果跨度ID重复或存在循环引用，抛出此异常。
    /// </exception>
    public void AddSpan(OtlpSpan span)
    {
        // 检查跨度ID是否已存在
        if (Spans.Contains(span.SpanId))
        {
            throw new InvalidOperationException($"Duplicate span id '{span.SpanId}' detected.");
        }

        // 标记跨度是否已插入
        var added = false;
        // 从后往前遍历跨度集合，按开始时间排序插入
        for (var i = Spans.Count - 1; i >= 0; i--)
        {
            if (span.StartTime > Spans[i].StartTime)
            {
                Spans.Insert(i + 1, span);
                added = true;
                break;
            }
        }
        // 如果未插入，则插入到集合头部
        if (!added)
        {
            Spans.Insert(0, span);
        }

        // 检查是否存在循环引用
        if (HasCircularReference(span))
        {
            // 移除存在循环引用的跨度
            Spans.Remove(span);
            throw new InvalidOperationException($"Circular loop detected for span '{span.SpanId}' with parent '{span.ParentSpanId}'.");
        }

        // 如果跨度没有父跨度ID
        if (string.IsNullOrEmpty(span.ParentSpanId))
        {
            // 遍历所有跨度，找到第一个没有父跨度ID的跨度作为根跨度
            foreach (var existingSpan in Spans)
            {
                if (string.IsNullOrEmpty(existingSpan.ParentSpanId))
                {
                    _rootSpan = existingSpan;
                    // 构建跟踪的完整名称
                    FullName = BuildFullName(existingSpan);
                    break;
                }
            }
        }
        // 如果根跨度为空且当前跨度是集合中的第一个跨度
        else if (_rootSpan == null && span == Spans[0])
        {
            // 使用第一个跨度构建跟踪的完整名称
            FullName = BuildFullName(span);
        }

        // 调试时检查跨度顺序
        AssertSpanOrder();
        static string BuildFullName(OtlpSpan existingSpan)
        {
            return $"{existingSpan.Source.Application.ApplicationName}: {existingSpan.Name}";
        }
    }

    /// <summary>
    /// 检查指定跨度是否存在循环引用。
    /// 从指定跨度开始向上遍历其父跨度，检查是否存在重复的跨度。
    /// </summary>
    /// <param name="span">要检查的跨度对象。</param>
    /// <returns>如果存在循环引用，返回true；否则返回false。</returns>
    private static bool HasCircularReference(OtlpSpan span)
    {
        // 如果跨度没有父跨度ID，则不存在循环引用
        if (string.IsNullOrEmpty(span.ParentSpanId))
        {
            return false;
        }

        // 用于存储已遍历的跨度
        var stack = new OtlpSpanCollection { span };
        // 从指定跨度开始向上遍历
        var currentSpan = span;
        // 当当前跨度有父跨度时，继续遍历
        while (currentSpan.GetParentSpan() is { } parentSpan)
        {
            // 如果父跨度已在栈中，则存在循环引用
            if (stack.Contains(parentSpan))
            {
                return true;
            }

            // 将父跨度添加到栈中
            stack.Add(parentSpan);
            // 更新当前跨度为父跨度
            currentSpan = parentSpan;
        }

        return false;
    }

    /// <summary>
    /// 在调试模式下，验证跟踪中所有跨度是否按开始时间排序。
    /// 如果发现跨度未按顺序排列，则抛出异常。
    /// </summary>
    /// <exception cref="InvalidOperationException">
    /// 如果跟踪中的跨度未按开始时间排序，抛出此异常。
    /// </exception>
    [Conditional("DEBUG")]
    private void AssertSpanOrder()
    {
        // 初始化当前时间为默认值
        DateTime current = default;
        // 遍历所有跨度
        for (var i = 0; i < Spans.Count; i++)
        {
            var span = Spans[i];
            // 如果当前跨度的开始时间早于前一个跨度的开始时间
            if (span.StartTime < current)
            {
                throw new InvalidOperationException($"Trace {TraceId} spans not in order at index {i}.");
            }

            // 更新当前时间为当前跨度的开始时间
            current = span.StartTime;
        }
    }

    /// <summary>
    /// 构造函数，初始化OtlpTrace对象。
    /// </summary>
    /// <param name="traceId">跟踪的唯一键，以只读内存字节形式表示。</param>
    public OtlpTrace(ReadOnlyMemory<byte> traceId)
    {
        // 初始化跟踪的唯一键
        Key = traceId;
        // 将唯一键转换为十六进制字符串作为跟踪ID
        TraceId = OtlpHelpers.ToHexString(traceId);
        // 初始化跟踪的完整名称为空字符串
        FullName = string.Empty;
    }

    public OtlpTrace(string traceId)
    {
        // 将唯一键转换为十六进制字符串作为跟踪ID
        TraceId = traceId;
        // 初始化跟踪的完整名称为空字符串
        FullName = string.Empty;
    }

    /// <summary>
    /// 克隆一个OtlpTrace对象。
    /// 复制跟踪的键，并克隆所有跨度。
    /// </summary>
    /// <param name="trace">要克隆的OtlpTrace对象。</param>
    /// <returns>克隆后的OtlpTrace对象。</returns>
    public static OtlpTrace Clone(OtlpTrace trace)
    {
        // 创建一个新的OtlpTrace对象，使用原跟踪的键
        var newTrace = new OtlpTrace(trace.Key);
        // 遍历原跟踪的所有跨度
        foreach (var item in trace.Spans)
        {
            // 克隆每个跨度并添加到新跟踪中
            newTrace.AddSpan(OtlpSpan.Clone(item, newTrace));
        }

        return newTrace;
    }

    /// <summary>
    /// 用于调试时显示跟踪的详细信息。
    /// 返回包含跟踪ID、跨度数量、开始日期、开始时间和持续时间的字符串。
    /// </summary>
    /// <returns>调试信息字符串。</returns>
    private string DebuggerToString()
    {
        return $@"TraceId = ""{TraceId}"", Spans = {Spans.Count}, StartDate = {FirstSpan?.StartTime.ToLocalTime():yyyy:MM:dd}, StartTime = {FirstSpan?.StartTime.ToLocalTime():h:mm:ss.fff tt}, Duration = {Duration}";
    }

    /// <summary>
    /// 实现IComparer<OtlpSpan>接口，用于比较两个跨度的开始时间。
    /// </summary>
    private sealed class SpanStartDateComparer : IComparer<OtlpSpan>
    {
        /// <summary>
        /// 静态只读实例，用于比较跨度的开始时间。
        /// </summary>
        public static readonly SpanStartDateComparer Instance = new SpanStartDateComparer();

        /// <summary>
        /// 比较两个跨度的开始时间。
        /// </summary>
        /// <param name="x">第一个跨度对象。</param>
        /// <param name="y">第二个跨度对象。</param>
        /// <returns>
        /// 如果x的开始时间早于y，返回负数；
        /// 如果x的开始时间等于y，返回0；
        /// 如果x的开始时间晚于y，返回正数。
        /// </returns>
        public int Compare(OtlpSpan? x, OtlpSpan? y)
        {
            return x!.StartTime.CompareTo(y!.StartTime);
        }
    }
}
